package bloodfang;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;

public class Stats {
	
	// holds cached stats and is getting reseted if something important happened...
	private HashMap<Stat,Double> statCache = new HashMap<Stat,Double>();
	private HashMap<String,Double> funcCache = new HashMap<String,Double>();
	
	// static object with data
	private final Procs procData = new Procs();
	
	private Settings settings;
	
	private HashSet<Talent> talents = new HashSet<Talent>();
	private HashSet<String> glyphs = new HashSet<String>();
	private HashSet<RaidBuff> raidBuffs = new HashSet<RaidBuff>();
	private HashSet<SetBonus> setBonuses = new HashSet<SetBonus>();
	private HashSet<ProcInfo> activeProcs = new HashSet<ProcInfo>();
	
	// baseStats do not change during calculation
	private final StatBlock gearStats;
	
	// procStats will be rewritten regularly
	private StatBlock procStats = new StatBlock();
	private Weapon mh;
	private Weapon oh;
	private Race race;
	
	// this is used to calculate EP Values for given Data
	private StatBlock epStats = new StatBlock();
	
	public Stats(Settings _settings, StatBlock _gearStats, Race _race, Weapon _mh, Weapon _oh, String _talents, HashSet<String> _glyphs, EnumSet<RaidBuff> _raidBuffs, EnumSet<SetBonus> _setBonuses, HashSet<ProcInfo> procs) {
		setSettings(_settings);
		gearStats = _gearStats;
		setRace(_race);
		setMh(_mh);
		setOh(_oh);
		setTalents(_talents);
		setGlyphs(_glyphs);
		raidBuffs.addAll(_raidBuffs);
		getSetBonuses().addAll(_setBonuses);
		this.activeProcs.addAll(procs);
	}

	public StatBlock getGearStats() {
		return gearStats;
	}
	
	public StatBlock getEpStats() {
		return epStats;
	}

	public void setEpStats(StatBlock epStats) {
		this.epStats = epStats;
	}

	public Weapon getMh() {
		return mh;
	}

	public void setMh(Weapon mh) {
		this.mh = mh;
	}

	public Weapon getOh() {
		return oh;
	}

	public void setOh(Weapon oh) {
		this.oh = oh;
	}
	
	public boolean hasSetBonus(SetBonus s){
		return getSetBonuses().contains(s);
	}
	
	public boolean hasRaidBuff(RaidBuff b){
		return raidBuffs.contains(b);
	}
	
	public boolean hasProc(String name){
		for(ProcInfo proc : activeProcs){
			if(proc.getProcName().equals(name))
				return true;
		}
		return false;
	}
	
	public ProcInfo getProcInfo(String name){
		for(ProcInfo proc : activeProcs){
			if(proc.getProcName().equals(name))
				return proc;
		}
		return null;
	}

	public double getBuffed(Stat stat) {
		return this.getCached(stat);
		//return this.calcBuffed(stat);
	}

	public double getMhHitChance() {
		return this.getCachedFunc("MhHitChance");
	}
	
	public double getOhHitChance() {
		return this.getCachedFunc("OhHitChance");
	}
	
	public double getMhStyleHitChance() {
		return this.getCachedFunc("MhStyleHitChance");
	}
	
	public double getOhStyleHitChance() {
		return this.getCachedFunc("OhStyleHitChance");
	}

	public double getEffectiveDamage(){
		return this.getCachedFunc("EffectiveDamage");
	}

	public double getEffectiveDamage(double armor){
		double armor_mitigation = armor / (armor + RATING.ARMOR_MITIGATION);
		return 1 - armor_mitigation;
	}
	
	public double getCritPercent(){
		return this.getCachedFunc("CritPercent");
	}
	
	public double getCritPercentStatic(){
		return this.getCachedFunc("CritPercentStatic");
	}
	
	public double getMultistrikeChance(){
		return this.getCachedFunc("MultistrikeChance");
	}
	
	public double getCooldownRecoveryRate() {
		return this.getCachedFunc("CooldownRecoveryRate");
	}
	
	public double getVersatilityMultiplier() {
		return this.getCachedFunc("VersatilityMultiplier");
	}

	public double getRealEnergyCost(double i) {
		double base = i * (0.8 + 0.2 / this.getMhStyleHitChance());
		if(settings.spec == Spec.COMBAT) {
			double mg_proc_chance = getMainGaucheProcChance();
			double combat_potency_proc_chance = 0.2;
			double avg_mg_cp_reduction = 15 * mg_proc_chance * combat_potency_proc_chance;
			base -= avg_mg_cp_reduction;
		}
		return base;
	}
	
	public double getMainGaucheProcChance(){
		return this.getCachedFunc("MainGaucheProcChance");
	}
	
	private double calcMainGaucheProcChance(){
		double mastery = 8 + this.getBuffed(Stat.mst) / RATING.MASTERY_CONVERSION_RATING;
		double mg_proc_chance = mastery * 0.02;
		return mg_proc_chance;
	}
	
	public double get_subtlety_mastery_multiplier(){
		double mastery = 8 + this.getBuffed(Stat.mst) / RATING.MASTERY_CONVERSION_RATING;
		return mastery * 0.03;
	}
	
	public double get_assassination_mastery_multiplier(){
		double mastery = 8 + this.getBuffed(Stat.mst) / RATING.MASTERY_CONVERSION_RATING;
		return mastery * 0.035;
	}

	public HashSet<ProcInfo> getProcs() {
		return activeProcs;
	}

	public void setProcs(HashSet<ProcInfo> procs) {
		this.activeProcs = procs;
	}

	public Settings getSettings() {
		return settings;
	}

	public void setSettings(Settings settings) {
		this.settings = settings;
	}
	
	public void setTalents(String talents){
		for(int row = 0; row < talents.length(); row++){
			char column = talents.charAt(row);
			if(column == '.') continue;
			int col = Integer.parseInt(String.valueOf(column));
			this.talents.add(Talent.get(row,col));
		}
	}
	
	public boolean hasTalent(Talent t){
		return talents.contains(t);
	}

	public StatBlock getProcStats() {
		return procStats;
	}
	
	public boolean hasGlyph(String glyph) {
		return glyphs.contains(glyph);
	}

	public void setGlyphs(HashSet<String> glyphs) {
		this.glyphs = glyphs;
	}

	public void setProcStats(StatBlock procStats) {
		this.procStats = procStats;
	}

	public Procs getProcData() {
		return procData;
	}

	public void toggleSetBonus(SetBonus s) {
		if(hasSetBonus(s))		
			getSetBonuses().remove(s);
		else
			getSetBonuses().add(s);
	}
	
	public void setRace(Race _race) {
		race = _race;
		if(race == Race.undead)
			activeProcs.add(new ProcInfo("touch_of_the_grave"));
		else if(hasProc("touch_of_the_grave"))
			activeProcs.remove(getProcInfo("touch_of_the_grave"));
	}

	public Race getRace() {
		return race;
	}

	public HashSet<SetBonus> getSetBonuses() {
		return setBonuses;
	}

	public void setSetBonuses(HashSet<SetBonus> setBonuses) {
		this.setBonuses = setBonuses;
	}
	
	public void reCache(){
		for(Stat stat : Stat.values()){
			reCache(stat);
		}
		funcCache.put("MhHitChance", this.calcMhHitChance());
		funcCache.put("OhHitChance", this.calcOhHitChance());
		funcCache.put("MhStyleHitChance", this.calcMhStyleHitChance());
		funcCache.put("OhStyleHitChance", this.calcOhStyleHitChance());
		funcCache.put("CritPercent", this.calcCritPercent());
		funcCache.put("CritPercentStatic", this.calcCritPercentStatic());
		funcCache.put("EffectiveDamage", this.calcEffectiveDamage());
		funcCache.put("MultistrikeChance", this.calcMultistrikeChance());
		funcCache.put("CooldownRecoveryRate", this.calcCooldownRecoveryRate());
		funcCache.put("VersatilityMultiplier", this.calcVersatilityMultiplier());
		funcCache.put("MainGaucheProcChance", this.calcMainGaucheProcChance());
	}
	
	private void reCache(Stat stat){
		statCache.put(stat, this.calcBuffed(stat));
	}
	
	private void reCacheFunc(String func){
		try {
			Method method = this.getClass().getDeclaredMethod( "calc"+func );
			double value = (double) method.invoke( this );
			funcCache.put(func, value);
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
			System.err.println("Error: The method " + e.getMessage() + " needs to be defined.");
			System.exit(1);
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		}
	}
	
	private double getCached(Stat stat){
		if(!settings.use_cache)
			return this.calcBuffed(stat);
		if(!statCache.containsKey(stat)){
			reCache(stat);
		}
		return statCache.get(stat);
	}
	
	private double getCachedFunc(String func){
		if(!settings.use_cache){
			try {
				Method method = this.getClass().getDeclaredMethod( "calc"+func );
				double value = (double) method.invoke( this );
				return value;
			} catch (SecurityException e) {
				e.printStackTrace();
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
				System.err.println("Error: The method " + e.getMessage() + " needs to be defined.");
				System.exit(1);
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}
		if(!funcCache.containsKey(func)){
			reCacheFunc(func);
		}
		return funcCache.get(func);
	}
	
	private double calcBuffed(Stat stat) {
		double base = gearStats.getStat(stat) + race.getStats().getStat(stat);
		base += getProcStats().getStat(stat) + epStats.getStat(stat);
		switch(stat){
			case str:
				if(hasRaidBuff(RaidBuff.allstats_buff))
					base *= 1.05;
				return base;
			case agi:
				if(hasRaidBuff(RaidBuff.agi_flask))
					base += 114;
				if(hasRaidBuff(RaidBuff.agi_food)){
					base += 34;
					if(race == Race.pandaren){
						base += 34;
					}
				}
				if(hasRaidBuff(RaidBuff.agi_pot))
					base += 456 * 25 / getSettings().FIGHT_LENGTH;
				if(hasRaidBuff(RaidBuff.agi_prepot))
					base += 456 * 23 / getSettings().FIGHT_LENGTH;
				base *= 1.05; // leather specialization
				if(hasRaidBuff(RaidBuff.allstats_buff))
					base *= 1.05;
				if(settings.spec == Spec.SUBTLETY)
					base *= 1.15;
				return base;
			case ap:
				base += getBuffed(Stat.str) + getBuffed(Stat.agi);
				if(hasRaidBuff(RaidBuff.attackpower_buff))
					base *= 1.1;
				if(settings.spec == Spec.COMBAT)
					base *= 1.40;
				return base;
			case hst:
				if(settings.spec == Spec.COMBAT) // attunement
					base *= 1.05;
				return base;
			case mst:
				if(hasRaidBuff(RaidBuff.mastery_buff))
					base += 118; // level 90
				if(settings.spec == Spec.ASSASSINATION) // attunement
					base *= 1.05;
				return base;
			case mul:
				if(settings.spec == Spec.SUBTLETY) // attunement
					base *= 1.05;
				return base;
			default:
				return base;
		}
	}
	
	private double calcMhHitChance() {
		double base_miss = RATING.BASE_STYLE_MISS_CHANCE + RATING.DUAL_WIELD_PENALTY;
		double base_dodge = RATING.BASE_DODGE_CHANCE;
		// perk
		if(settings.spec == Spec.COMBAT && settings.player_level >= 100)
			base_miss -= RATING.DUAL_WIELD_PENALTY;
		
		return (100-base_miss-base_dodge)/100;
	}
	
	private double calcOhHitChance() {
		double base_miss = RATING.BASE_STYLE_MISS_CHANCE + RATING.DUAL_WIELD_PENALTY;
		double base_dodge = RATING.BASE_DODGE_CHANCE;
		// perk
		if(settings.spec == Spec.COMBAT  && settings.player_level >= 100)
			base_miss -= RATING.DUAL_WIELD_PENALTY;
		
		return (100-base_miss-base_dodge)/100;
	}
	
	private double calcMhStyleHitChance() {
		double base_miss = RATING.BASE_STYLE_MISS_CHANCE;
		double base_dodge = RATING.BASE_DODGE_CHANCE;
		
		return (100-base_miss-base_dodge)/100;
	}
	
	private double calcOhStyleHitChance() {
		double base_miss = RATING.BASE_STYLE_MISS_CHANCE;
		double base_dodge = RATING.BASE_DODGE_CHANCE;
		
		return (100-base_miss-base_dodge)/100;
	}
	
	private double calcCritPercent(){
		double base_crit = 15.; // assumes to be a rogue
		double crit_reduction = -3.0;
		double crit_buffs = 0.0;
		if(hasRaidBuff(RaidBuff.crit_buff))
			crit_buffs += 5.0;
		if(race == Race.worgen)
			crit_buffs += 1.0;
		return (base_crit + this.getBuffed(Stat.cri)/RATING.CRIT_CONVERSION_RATING + crit_buffs + crit_reduction) / 100.;
	}
	
	private double calcCritPercentStatic(){
		double base_crit = 15.; // assumes to be a rogue
		double crit_reduction = -3.0;
		double crit_buffs = 0.0;
		if(hasRaidBuff(RaidBuff.crit_buff))
			crit_buffs += 5.0;
		if(race == Race.worgen)
			crit_buffs += 1.0;
		return (base_crit + gearStats.getStat(Stat.cri)/RATING.CRIT_CONVERSION_RATING + crit_buffs + crit_reduction) / 100.;
	}
	
	private double calcMultistrikeChance(){
		double chance = this.getBuffed(Stat.mul) / RATING.MULTISTRIKE_CONVERSION_RATING / 100.;
		if(hasRaidBuff(RaidBuff.multistrike_buff)) {
			chance += 0.05;
		}
		return chance;
	}
	
	private double calcEffectiveDamage(){
		return getEffectiveDamage(RATING.BOSS_ARMOR);
	}

	private double calcCooldownRecoveryRate() {
		ProcInfo procInfo = null;
        for(String prefix : Procs.trinket_prefixes){
        	if(hasProc(prefix+"assurance_of_consequence"))
				procInfo = getProcInfo(prefix+"assurance_of_consequence");
        }
        double res = 0.;
        if(procInfo != null){
            ProcData procData = getProcData().get(procInfo.getProcName());
            double scale = Cycle.get_scaling_value(procData,procInfo.getUpgradeLevel());
            res = scale * 0.0098999999 / 100;
        }
		double rate = this.getBuffed(Stat.rea) / RATING.READINESS_CONVERSION_RATING / 100.;
		return rate + res;
	}
	
	private double calcVersatilityMultiplier() {
		double rate = this.getBuffed(Stat.ver) / RATING.VERSATILITY_CONVERSION_RATING / 100.;
		if(hasRaidBuff(RaidBuff.versatility_buff)) 
			rate += 0.03;
		return 1 + rate;
	}
}
